package algo3.algothief.modelo;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Random;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;


public class Mundo {

	private ArrayList<Pais> paises;
	
	// La matriz de adyacencias vendria a hacer la representacion de un grafo
	// que tiene por nodos a los Paises y por aristas a los posibles viajes
	// que se pueden realizar entre los Paises. 
	// Como desde cualquier Pais al que se viaje se debe poder volver al anterior,
	// con esta representacion, la matriz de adyacencias DEBE ser simetrica.
	private int[][] matrizDeAdyacencias; 
	
	// Las cantidades minima y maxima de Paises, representan los valores entre los
	// cuales puede estar la cantidad de Paises a los que se puede viajar desde un
	// determinado Pais.
	private static int cantidadMinimaDePaises = 3;
	private static int cantidadMaximaDePaises = 4;
	
//-------------------------------------------------------------------------------------

	public Mundo(String direccionDeLosPaises) throws ParserConfigurationException, SAXException, IOException {
		Document doc = this.cargarDocumentoNormalizado(direccionDeLosPaises);
		this.cargarMundo(doc);
		
		this.matrizDeAdyacencias = this.crearLaMatrizDeAdyacencias();
		this.definirLosPosiblesViajesEntrePaises();
	}
	
	private Mundo(ArrayList<Pais> paisesNuevos, int[][] matrizDeAdyacenciasNueva) {
		this.paises = paisesNuevos;
		this.matrizDeAdyacencias = matrizDeAdyacenciasNueva;
	}
	
//-------------------------------------------------------------------------------------
	
	// La forma de serializar esta escrita lo mas generica posible.
	public Element serializar(Document doc) {
		Element elementoMundo = doc.createElement("Mundo");
		
		Element elementoMatriz = this.serializarMatriz(doc);
		elementoMundo.appendChild(elementoMatriz);
		
		Element elementoPaises = this.serializarPaises(doc);
		elementoMundo.appendChild(elementoPaises);
		
		return elementoMundo;
	}

	public static Mundo hidratar(Document doc) throws ParserConfigurationException, SAXException, IOException {
		Element elementoMundo = (Element)doc.getElementsByTagName("Mundo").item(0);
		
		Element elementoPaises = (Element)doc.getElementsByTagName("PaisesDelMundo").item(0);
		ArrayList<Pais> paisesDelMundoNuevo = Mundo.hidratarPaises(elementoPaises, doc);
		int cantidadDePaises = paisesDelMundoNuevo.size();

		Element elementoMatriz = (Element)elementoMundo.getElementsByTagName("MatrizDeAdyacencias").item(0);
		int[][] matrizDeAdyacencias = Mundo.hidratarMatriz(elementoMatriz, cantidadDePaises);
		
		Mundo mundoNuevo = new Mundo(paisesDelMundoNuevo, matrizDeAdyacencias);
		
		return mundoNuevo;
	}
	
//-------------------------------------------------------------------------------------

	private Document cargarDocumentoNormalizado(String direccionDelArchivo) throws ParserConfigurationException, SAXException, IOException {
		File archivo = new File(direccionDelArchivo);

		// Se traduce el archivo a algo manejable para el programa.
		DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
		Document doc = dBuilder.parse(archivo);
		doc.getDocumentElement().normalize();
		
		return doc;
	}
	
	private void cargarMundo(Document doc) throws ParserConfigurationException, SAXException, IOException{
		Element elementoMundo = (Element)doc.getElementsByTagName("Mundo").item(0);
		
		NodeList elementosPaises = elementoMundo.getElementsByTagName("Pais");
		int cantidadDePaises = elementosPaises.getLength();
		
		this.paises = new ArrayList<Pais>();
		
		for(int i = 0; i < cantidadDePaises; i++) {
			String direccionDelPais = ((Element)elementosPaises.item(i)).getAttribute("direccion");
			
			Pais pais = new Pais(direccionDelPais);
			paises.add(pais);
		}
	}
	

	
	private String obtenerCerosYUnosDeLaFila(Integer indiceDeLaFila) {
		String cerosYUnos = "";
		
		int cantidadDePaises = paises.size();
		for (int j = 0; j < cantidadDePaises; j++) {
			String ceroOUno = Integer.toString(matrizDeAdyacencias[indiceDeLaFila][j]);
			cerosYUnos += ceroOUno;
		}
		
		return cerosYUnos;
	}
	
	private Element serializarMatriz(Document doc) {
		Element elementoMatriz = doc.createElement("MatrizDeAdyacencias");
		
		Integer cantidadDePaises = paises.size();
		for (Integer i = 1; i < cantidadDePaises + 1; i++) {
			Element elementoFila = doc.createElement("Fila-" + i.toString());
			
			String filaConCerosYUnos = this.obtenerCerosYUnosDeLaFila(i-1);
			elementoFila.setAttribute("columnas", filaConCerosYUnos);
			
			elementoMatriz.appendChild(elementoFila);
		}
		
		return elementoMatriz;
	}
	
	private Element serializarPaises(Document doc) {
		Integer cantidadDePaises = paises.size();
		
		Element elementoPaises = doc.createElement("PaisesDelMundo");
		elementoPaises.setAttribute("cantidadDePaises", cantidadDePaises.toString());
		
		for (Integer i = 1; i < cantidadDePaises + 1; i++) {
			Pais pais = paises.get(i-1);
			Element elementoPais = pais.serializar(doc, "DelMundo" + "-" + i.toString());
			elementoPaises.appendChild(elementoPais);
		}
		
		return elementoPaises;
	}
	
	
	
	private static ArrayList<Pais> hidratarPaises(Element elementoPaises, Document doc) throws ParserConfigurationException, SAXException, IOException {
		ArrayList<Pais> paisesNuevos = new ArrayList<Pais>();
		
		String cantidadDePaisesEnFormaDeString = elementoPaises.getAttribute("cantidadDePaises");
		Integer cantidadDePaises = Integer.parseInt(cantidadDePaisesEnFormaDeString);
		
		for (Integer i = 1; i < cantidadDePaises + 1; i++) {
			Pais paisNuevo = Pais.hidratar(doc, "DelMundo" + "-" + i.toString());
			paisesNuevos.add(paisNuevo);
		}
		
		return paisesNuevos;
	}
	
	private static int[][] hidratarMatriz(Element elementoMatriz, int cantidadDePaises) {
		int[][] matriz = new int[cantidadDePaises][cantidadDePaises];
	
		for (int i = 1; i < cantidadDePaises + 1; i++) {
			Element elementoFila = (Element)elementoMatriz.getElementsByTagName("Fila-" + i).item(0);
			String cerosOUnos = elementoFila.getAttribute("columnas");
			for (int j = 0; j < cantidadDePaises; j++) {
				int ceroOUno = cerosOUnos.charAt(j) - '0';
				matriz[i-1][j] = ceroOUno;
			}
		}
			
		return matriz;
	}
	
	
	
	private int[][] crearLaMatrizDeAdyacencias() {
		int cantidadDePaises = paises.size();
		int[][] matriz = new int[cantidadDePaises][cantidadDePaises];
		
		// La matriz se llena de 0 (que representan que no hay viaje posible 
		// entre los Paises de las coordenadas (i,j)).
		for (int i = 0; i < cantidadDePaises; i++)
			for (int j = 0; j < cantidadDePaises; j++)
				matriz[i][j] = 0;
		
		return matriz;
	}
	
	private int generarEnteroAlAzarEntre(int minimoValor, int maximoValor) {
		Random generadorAleatorio = new Random();
		int numeroAlAzar = generadorAleatorio.nextInt(maximoValor - minimoValor + 1) + minimoValor;
	
		return numeroAlAzar;
	}
	
	private ArrayList<Integer> verPaisesSinUnosDisponibles(int indiceDelPais, int[] cantidadDeUnosPorFila, int[] cantidadDePaisesPosiblesALosQueSePuedeViajarPorFila) {
		ArrayList<Integer> paisesSinUnos = new ArrayList<Integer>();
		
		int cantidadDePaisesDelMundo = paises.size();
		for (int j = 0; j < cantidadDePaisesDelMundo; j++) {
			int ceroOUno = matrizDeAdyacencias[indiceDelPais][j];
			
			if ((ceroOUno == 0) && (indiceDelPais != j) && (cantidadDeUnosPorFila[j] < cantidadDePaisesPosiblesALosQueSePuedeViajarPorFila[j]))
				paisesSinUnos.add(j);
		}
		
		return paisesSinUnos;
	}
	
	private int generarEnteroAlAzarEntreLosValoresDe(ArrayList<Integer> paisesSinUnos) {
		int cantidadDePaisesSinUnos = paisesSinUnos.size();
		int numeroAlAzar = this.generarEnteroAlAzarEntre(0, cantidadDePaisesSinUnos - 1);
	
		return paisesSinUnos.get(numeroAlAzar);
	}
	
	private void definirLosPosiblesViajesEntrePaises() {
		// La matriz se llena de cierta cantidad de 1 por fila de manera 
		// aleatoria (que representan que es posible viajar entre los Paises 
		// de las coordenadas (i,j)). Como la matriz debe ser simetrica, esta 
		// el arreglo cantidadDeUnosPorFila que cuenta la cantidad de 1 que
		// hay en cada fila, cuya maxima cantidad de 1 esta dada por el arreglo
		// cantidadDePaisesPosiblesALosQueSePuedeViajarPorFila.
		int cantidadDePaises = paises.size();
		
		int[] cantidadDeUnosPorFila = new int[cantidadDePaises];
		for (int i = 0; i < cantidadDePaises; i++) 
			cantidadDeUnosPorFila[i] = 0;
		
		int[] cantidadDePaisesPosiblesALosQueSePuedeViajarPorFila = new int[cantidadDePaises];
		for (int i = 0; i < cantidadDePaises; i++) 
			cantidadDePaisesPosiblesALosQueSePuedeViajarPorFila[i] = this.generarEnteroAlAzarEntre(cantidadMinimaDePaises,cantidadMaximaDePaises); 
		
	
		for (int i = 0; i < cantidadDePaises; i++) {
			int cantidadDePaisesPosiblesALosQueSePuedeViajar = cantidadDePaisesPosiblesALosQueSePuedeViajarPorFila[i];
			
			for (int k = 0; k < cantidadDePaisesPosiblesALosQueSePuedeViajar; k++) 
				if (cantidadDeUnosPorFila[i] < cantidadDePaisesPosiblesALosQueSePuedeViajarPorFila[i]) { 
					ArrayList<Integer> paisesSinUnos = this.verPaisesSinUnosDisponibles(i, cantidadDeUnosPorFila, cantidadDePaisesPosiblesALosQueSePuedeViajarPorFila);
					
					if (paisesSinUnos.size() != 0) {
						int j = this.generarEnteroAlAzarEntreLosValoresDe(paisesSinUnos);
					
						matrizDeAdyacencias[i][j] = 1;
						cantidadDeUnosPorFila[i]++;
			
						matrizDeAdyacencias[j][i] = 1;
						cantidadDeUnosPorFila[j]++;
					}
				}
		}
	}
	
	
	private int buscarIndiceDelPaisInicial(Pais paisInicial) {
		int indiceDelPaisInicial = -1;
		int cantidadDePaises = paises.size();
		
		for (int i = 0; ( (indiceDelPaisInicial == -1) && (i < cantidadDePaises) ); i++) {
			String nombreDelPaisActual = paises.get(i).verNombreDeLaCapital();
			String nombreDelPaisInicial = paisInicial.verNombreDeLaCapital();
			
			if (nombreDelPaisActual.matches(nombreDelPaisInicial)) 
				indiceDelPaisInicial = i;
		}
		
		return indiceDelPaisInicial;
	}
	
	private boolean existeUnaConexion(int conexion) {
		return (conexion == 1);
	}
	
//-------------------------------------------------------------------------------------
	
	public ArrayList<Pais> verPaisesDelMundo() {
		return this.paises;
	}
	
	public ArrayList<Pais> verLosPosiblesPaisesALosQueSePuedeViajar(Pais paisInicial) {
		ArrayList<Pais> posiblesPaisesParaViajar = new ArrayList<Pais>();
		
		int indiceDelPaisInicial = this.buscarIndiceDelPaisInicial(paisInicial);
		if (indiceDelPaisInicial == -1) return posiblesPaisesParaViajar;
		
		int cantidadDePaises = paises.size();
		
		for (int j = 0; j < cantidadDePaises; j++) {
			int conexion = matrizDeAdyacencias[indiceDelPaisInicial][j];
			boolean existeConexion = this.existeUnaConexion(conexion);
			
			if (existeConexion) {
				Pais posiblePaisParaViajar = paises.get(j);
				posiblesPaisesParaViajar.add(posiblePaisParaViajar);
			}
		}
		
		return posiblesPaisesParaViajar;
	}
}
